home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Freeware / Miro 1.0 / Miro_Installer.exe / xulrunner / python / dl_daemon / download.py < prev    next >
Encoding:
Python Source  |  2007-11-12  |  27.9 KB  |  844 lines

  1. import os
  2. import sys
  3. import bsddb
  4. import shutil
  5. import types
  6. from gettext import gettext as _
  7. from os import remove
  8. import re
  9. from threading import RLock, Event, Thread
  10. import traceback
  11. from copy import copy
  12.  
  13. from BitTorrent.bencode import bdecode
  14. from clock import clock
  15. from download_utils import cleanFilename, nextFreeFilename, checkFilenameExtension, filterDirectoryName
  16. from download_utils import filenameFromURL, getFileURLPath
  17. import eventloop
  18. import httpclient
  19. import datetime
  20. import logging
  21. import fileutil
  22.  
  23. import config
  24. import prefs
  25. from sha import sha
  26.  
  27. from dl_daemon import command, daemon, bittorrentdtv
  28. from datetime import timedelta
  29. from util import checkF, checkU, stringify
  30.  
  31. import platformutils
  32. import string
  33.  
  34. chatter = True
  35.  
  36. # a hash of download ids to downloaders
  37. _downloads = {}
  38.  
  39. _lock = RLock()
  40.  
  41. def createDownloader(url, contentType, dlid):
  42.     checkU(url)
  43.     checkU(contentType)
  44.     if contentType == u'application/x-bittorrent':
  45.         return BTDownloader(url, dlid)
  46.     else:
  47.         return HTTPDownloader(url, dlid)
  48.  
  49. # Creates a new downloader object. Returns id on success, None on failure
  50. def startNewDownload(url, dlid, contentType, channelName):
  51.     checkU(url)
  52.     checkU(contentType)
  53.     if channelName:
  54.         checkF(channelName)
  55.     dl = createDownloader(url, contentType, dlid)
  56.     dl.channelName = channelName
  57.     _downloads[dlid] = dl
  58.  
  59. def pauseDownload(dlid):
  60.     try:
  61.         download = _downloads[dlid]
  62.     except: # There is no download with this id
  63.         return True
  64.     return download.pause()
  65.  
  66. def startDownload(dlid):
  67.     try:
  68.         download = _downloads[dlid]
  69.     except KeyError:  # There is no download with this id
  70.         err= u"in startDownload(): no downloader with id %s" % dlid
  71.         c = command.DownloaderErrorCommand(daemon.lastDaemon, err)
  72.         c.send()
  73.         return True
  74.     return download.start()
  75.  
  76. def stopDownload(dlid, delete):
  77.     try:
  78.         _lock.acquire()
  79.         try:
  80.             download = _downloads[dlid]
  81.             del _downloads[dlid]
  82.         finally:
  83.             _lock.release()
  84.     except: # There is no download with this id
  85.         return True
  86.     return download.stop(delete)
  87.  
  88. def stopUpload(dlid):
  89.     try:
  90.         _lock.acquire()
  91.         try:
  92.             download = _downloads[dlid]
  93.             if download.state != u"uploading":
  94.                 return
  95.             del _downloads[dlid]
  96.         finally:
  97.             _lock.release()
  98.     except: # There is no download with this id
  99.         return
  100.     return download.stopUpload()
  101.  
  102. def migrateDownload(dlid, directory):
  103.     checkF(directory)
  104.     try:
  105.         download = _downloads[dlid]
  106.     except: # There is no download with this id
  107.         pass
  108.     else:
  109.         if download.state in (u"finished", u"uploading"):
  110.             download.moveToDirectory(directory)
  111.  
  112. def getDownloadStatus(dlids = None):
  113.     statuses = {}
  114.     for key in _downloads.keys():
  115.         if ((dlids is None)  or (dlids == key) or (key in dlids)):
  116.             try:
  117.                 statuses[key] = _downloads[key].getStatus()
  118.             except:
  119.                 pass
  120.     return statuses
  121.  
  122. def shutDown():
  123.     logging.info ("Shutting down downloaders...")
  124.     for dlid in _downloads:
  125.         _downloads[dlid].shutdown()
  126.  
  127. def restoreDownloader(downloader):
  128.     downloader = copy(downloader)
  129.     dlerType = downloader.get('dlerType')
  130.     if dlerType == u'HTTP':
  131.         dl = HTTPDownloader(restore = downloader)
  132.     elif dlerType == u'BitTorrent':
  133.         dl = BTDownloader(restore = downloader)
  134.     else:
  135.         err = u"in restoreDownloader(): unknown dlerType: %s" % dlerType
  136.         c = command.DownloaderErrorCommand(daemon.lastDaemon, err)
  137.         c.send()
  138.         return
  139.  
  140.     _downloads[downloader['dlid']] = dl
  141.  
  142. class DownloadStatusUpdater:
  143.     """Handles updating status for all in progress downloaders.
  144.  
  145.     On OS X and gtk if the user is on the downloads page and has a bunch of
  146.     downloads going, this can be a fairly CPU intensive task.
  147.     DownloadStatusUpdaters mitigate this in 2 ways.
  148.  
  149.     1) DownloadStatusUpdater objects batch all status updates into one big
  150.     update which takes much less CPU.  
  151.     
  152.     2) The update don't happen fairly infrequently (currently every 5 seconds).
  153.     
  154.     Becouse updates happen infrequently, DownloadStatusUpdaters should only be
  155.     used for progress updates, not events like downloads starting/finishing.
  156.     For those just call updateClient() since they are more urgent, and don't
  157.     happen often enough to cause CPU problems.
  158.     """
  159.  
  160.     UPDATE_CLIENT_INTERVAL = 5
  161.  
  162.     def __init__(self):
  163.         self.toUpdate = set()
  164.  
  165.     def startUpdates(self):
  166.         eventloop.addTimeout(self.UPDATE_CLIENT_INTERVAL, self.doUpdate,
  167.                 "Download status update")
  168.  
  169.     def doUpdate(self):
  170.         try:
  171.             statuses = []
  172.             for downloader in self.toUpdate:
  173.                 statuses.append(downloader.getStatus())
  174.             self.toUpdate = set()
  175.             if statuses:
  176.                 command.BatchUpdateDownloadStatus(daemon.lastDaemon, 
  177.                         statuses).send()
  178.         finally:
  179.             eventloop.addTimeout(self.UPDATE_CLIENT_INTERVAL, self.doUpdate,
  180.                     "Download status update")
  181.  
  182.     def queueUpdate(self, downloader):
  183.         self.toUpdate.add(downloader)
  184.  
  185. downloadUpdater = DownloadStatusUpdater()
  186.  
  187. RETRY_TIMES = (
  188.     60,
  189.     5 * 60,
  190.     10 * 60,
  191.     30 * 60,
  192.     60 * 60,
  193.     2 * 60 * 60,
  194.     6 * 60 * 60,
  195.     24 * 60 * 60
  196.     )
  197.  
  198. class BGDownloader:
  199.     def __init__(self, url, dlid):
  200.         self.dlid = dlid
  201.         self.url = url
  202.         self.startTime = clock()
  203.         self.endTime = self.startTime
  204.         self.shortFilename = filenameFromURL(url)
  205.         self.pickInitialFilename()
  206.         self.state = u"downloading"
  207.         self.currentSize = 0
  208.         self.totalSize = -1
  209.         self.blockTimes = []
  210.         self.shortReasonFailed = self.reasonFailed = u"No Error"
  211.         self.retryTime = None
  212.         self.retryCount = -1
  213.  
  214.     def getURL(self):
  215.         return self.url
  216.  
  217.     def getStatus(self):
  218.         return {'dlid': self.dlid,
  219.             'url': self.url,
  220.             'state': self.state,
  221.             'totalSize': self.totalSize,
  222.             'currentSize': self.currentSize,
  223.             'eta': self.getETA(),
  224.             'rate': self.getRate(),
  225.             'uploaded': 0,
  226.             'filename': self.filename,
  227.             'startTime': self.startTime,
  228.             'endTime': self.endTime,
  229.             'shortFilename': self.shortFilename,
  230.             'reasonFailed': self.reasonFailed,
  231.             'shortReasonFailed': self.shortReasonFailed,
  232.             'dlerType': None,
  233.             'retryTime': self.retryTime,
  234.             'retryCount': self.retryCount,
  235.             'channelName': self.channelName}
  236.  
  237.     def updateClient(self):
  238.         x = command.UpdateDownloadStatus(daemon.lastDaemon, self.getStatus())
  239.         return x.send()
  240.         
  241.     ##
  242.     def pickInitialFilename(self):
  243.         """Pick a path to download to based on self.shortFilename.
  244.  
  245.         This method sets self.filename, as well as creates any leading paths
  246.         needed to start downloading there.
  247.         """
  248.  
  249.         downloadDir = os.path.join(config.get(prefs.MOVIES_DIRECTORY),
  250.                 'Incomplete Downloads')
  251.         # Create the download directory if it doesn't already exist.
  252.         try:
  253.             os.makedirs(downloadDir)
  254.         except:
  255.             pass
  256.         cleaned = cleanFilename(self.shortFilename+".part")
  257.         self.filename = nextFreeFilename(os.path.join(downloadDir, cleaned))
  258.  
  259.     def moveToMoviesDirectory(self):
  260.         """Move our downloaded file from the Incomplete Downloads directoy to
  261.         the movies directory.
  262.         """
  263.         if chatter:
  264.             logging.info ("moving to movies directory filename is %s", self.filename)
  265.         self.moveToDirectory(config.get(prefs.MOVIES_DIRECTORY))
  266.  
  267.     def moveToDirectory (self, directory):
  268.         checkF(directory)
  269.         if self.channelName:
  270.             channelName = filterDirectoryName(self.channelName)
  271.             directory = os.path.join (directory, channelName)
  272.             try:
  273.                 os.makedirs(directory)
  274.             except:
  275.                 pass
  276.         newfilename = os.path.join(directory, self.shortFilename)
  277.         if newfilename == self.filename:
  278.             return
  279.         newfilename = nextFreeFilename(newfilename)
  280.         def callback():
  281.             self.filename = newfilename
  282.             self.updateClient()
  283.         fileutil.migrate_file(self.filename, newfilename, callback)
  284.  
  285.     ##
  286.     # Returns a float with the estimated number of seconds left
  287.     def getETA(self):
  288.         if self.totalSize == -1:
  289.             return -1
  290.         rate = self.getRate()
  291.         if rate > 0:
  292.             return (self.totalSize - self.currentSize)/rate
  293.         else:
  294.             return 0
  295.  
  296.     ##
  297.     # Returns a float with the download rate in bytes per second
  298.     def getRate(self):
  299.         now = clock()
  300.         if self.endTime != self.startTime:
  301.             rate = self.currentSize/(self.endTime-self.startTime)
  302.         else:
  303.             haltedSince = now
  304.             for time, size in reversed(self.blockTimes):
  305.                 if size == self.currentSize:
  306.                     haltedSince = time
  307.                 else:
  308.                     break
  309.             if now - haltedSince > self.HALTED_THRESHOLD:
  310.                 rate = 0
  311.             else:
  312.                 try:
  313.                     timespan = now - self.blockTimes[0][0]
  314.                     if timespan != 0:
  315.                         endSize = self.blockTimes[-1][1]
  316.                         startSize = self.blockTimes[0][1]
  317.                         rate = (endSize - startSize) / timespan
  318.                     else:
  319.                         rate = 0
  320.                 except IndexError:
  321.                     rate = 0
  322.         return rate
  323.  
  324.     def retryDownload(self):
  325.         self.retryDC = None
  326.         self.start()
  327.  
  328.     def handleTemporaryError(self, shortReason, reason):
  329.         self.state = u"offline"
  330.         self.reasonFailed = reason
  331.         self.shortReasonFailed = shortReason
  332.         self.retryCount = self.retryCount + 1
  333.         if self.retryCount >= len (RETRY_TIMES):
  334.             self.retryCount = len (RETRY_TIMES) - 1
  335.         self.retryDC = eventloop.addTimeout (RETRY_TIMES[self.retryCount], self.retryDownload, "Logarithmic retry")
  336.         self.retryTime = datetime.datetime.now() + datetime.timedelta(seconds = RETRY_TIMES[self.retryCount])
  337.         self.updateClient()
  338.  
  339.     def handleError(self, shortReason, reason):
  340.         self.state = u"failed"
  341.         self.reasonFailed = reason
  342.         self.shortReasonFailed = shortReason
  343.         self.updateClient()
  344.  
  345.     def handleNetworkError(self, error):
  346.         if isinstance(error, httpclient.NetworkError):
  347.             if isinstance (error, httpclient.MalformedURL) or \
  348.                isinstance (error, httpclient.UnexpectedStatusCode):
  349.                 self.handleError(error.getFriendlyDescription(),
  350.                                  error.getLongDescription())
  351.             else:
  352.                 self.handleTemporaryError(error.getFriendlyDescription(),
  353.                                           error.getLongDescription())
  354.         else:
  355.             logging.info ("WARNING: grabURL errback not called with NetworkError")
  356.             self.handleError(str(error), str(error))
  357.  
  358.     def handleGenericError(self, longDescription):
  359.         self.handleError(_("Error"), longDescription)
  360.  
  361.  
  362. class HTTPDownloader(BGDownloader):
  363.     UPDATE_CLIENT_WINDOW = 12
  364.     HALTED_THRESHOLD = 3 # how many secs until we consider a download halted
  365.  
  366.     def __init__(self, url = None,dlid = None,restore = None):
  367.         self.retryDC = None
  368.         self.channelName = None
  369.         if restore is not None:
  370.             if not isinstance(restore.get('totalSize', 0), int):
  371.                 # Sometimes restoring old downloaders caused errors because
  372.                 # their totalSize wasn't an int.  (see #3965)
  373.                 restore = None
  374.         if restore is not None:
  375.             self.__dict__.update(restore)
  376.             self.blockTimes = []
  377.             self.restartOnError = True
  378.         else:
  379.             BGDownloader.__init__(self, url, dlid)
  380.             self.restartOnError = False
  381.         self.client = None
  382.         self.filehandle = None
  383.         if self.state == 'downloading':
  384.             self.startDownload()
  385.         elif self.state == 'offline':
  386.             self.start()
  387.         else:
  388.             self.updateClient()
  389.  
  390.     def resetBlockTimes(self):
  391.         self.blockTimes = [(clock(), self.currentSize)]
  392.  
  393.     def startNewDownload(self):
  394.         self.currentSize = 0
  395.         self.totalSize = -1
  396.         self.startDownload()
  397.  
  398.     def startDownload(self):
  399.         if self.retryDC:
  400.             self.retryDC.cancel()
  401.             self.retryDC = None
  402.         if self.currentSize == 0:
  403.             headerCallback = self.onHeaders
  404.         else:
  405.             headerCallback = self.onHeadersRestart
  406.         self.client = httpclient.grabURL(self.url,
  407.                 self.onDownloadFinished, self.onDownloadError,
  408.                 headerCallback, self.onBodyData, start=self.currentSize)
  409.         self.resetBlockTimes()
  410.         self.updateClient()
  411.  
  412.     def cancelRequest(self):
  413.         if self.client is not None:
  414.             self.client.cancel()
  415.             self.client = None
  416.  
  417.     def handleError(self, shortReason, reason):
  418.         BGDownloader.handleError(self, shortReason, reason)
  419.         self.cancelRequest()
  420.         try:
  421.             remove (self.filename)
  422.         except:
  423.             pass
  424.         self.currentSize = 0
  425.         self.totalSize = -1
  426.  
  427.     def handleTemporaryError(self, shortReason, reason):
  428.         BGDownloader.handleTemporaryError(self, shortReason, reason)
  429.         self.cancelRequest()
  430.  
  431.     def handleWriteError(self, error):
  432.         self.handleGenericError(_("Could not write to %s") % 
  433.                                   stringify(self.filename))
  434.         if self.filehandle is not None:
  435.             try:
  436.                 self.filehandle.close()
  437.             except:
  438.                 pass
  439.         try:
  440.             remove(self.filename)
  441.         except:
  442.             pass
  443.  
  444.     def onHeaders(self, info):
  445.         if info['contentLength'] != None:
  446.             self.totalSize = info['contentLength']
  447.         if self.client.gotBadStatusCode:
  448.             error = httpclient.UnexpectedStatusCode(info['status'])
  449.             self.handleNetworkError(error)
  450.             return
  451.         if not self.acceptDownloadSize(self.totalSize):
  452.             self.handleError(_("Not enough disk space"),
  453.                 _("%s MB required to store this video") % 
  454.                 (self.totalSize / (2 ** 20)))
  455.             return
  456.         #We have a success
  457.         self.retryCount = -1
  458.         #Get the length of the file, then create it
  459.         self.shortFilename = cleanFilename(info['filename'])
  460.         self.shortFilename = checkFilenameExtension(self.shortFilename, info)
  461.         self.pickInitialFilename()
  462.         try:
  463.             self.filehandle = file(self.filename,"w+b")
  464.         except IOError:
  465.             self.handleGenericError("Couldn't open %s for writing" % 
  466.                 stringify(self.filename))
  467.             return
  468.         if self.totalSize > 0:
  469.             try:
  470.                 self.filehandle.seek(self.totalSize-1)
  471.                 self.filehandle.write(' ')
  472.                 self.filehandle.seek(0)
  473.             except IOError, error:
  474.                 self.handleWriteError(error)
  475.                 return
  476.         self.updateClient()
  477.  
  478.     def onHeadersRestart(self, info):
  479.         self.restartOnError = False
  480.         if info['status'] != 206 or 'content-range' not in info:
  481.             self.currentSize = 0
  482.             self.totalSize = -1
  483.             self.resetBlockTimes()
  484.             if not self.client.gotBadStatusCode:
  485.                 self.onHeaders(info)
  486.             else:
  487.                 self.cancelRequest()
  488.                 self.startNewDownload()
  489.             return
  490.         try:
  491.             self.parseContentRange(info['content-range'])
  492.         except ValueError:
  493.             logging.info ("WARNING, bad content-range: %r", info['content-range'])
  494.             logging.info ("currentSize: %d totalSize: %d", self.currentSize,
  495.                           self.totalSize)
  496.             self.cancelRequest()
  497.             self.startNewDownload()
  498.         else:
  499.             try:
  500.                 self.filehandle = file(self.filename,"r+b")
  501.                 self.filehandle.seek(self.currentSize)
  502.             except IOError, e:
  503.                 self.handleWriteError(e)
  504.             #We have a success
  505.             self.retryCount = -1
  506.         self.updateClient()
  507.  
  508.     def parseContentRange(self, contentRange):
  509.         """Parse the content-range header from an http response.  If it's
  510.         badly formatted, or it's not what we were expecting based on the state
  511.         we restored to, raise a ValueError.
  512.         """
  513.  
  514.         m = re.search('bytes\s+(\d+)-(\d+)/(\d+)', contentRange)
  515.         if m is None:
  516.             raise ValueError()
  517.         start = int(m.group(1))
  518.         end = int(m.group(2))
  519.         totalSize = int(m.group(3))
  520.         if start > self.currentSize or (end + 1 != totalSize):
  521.             # we only have the 1st <self.currentSize> bytes of the file, so
  522.             # we cant handle these responses
  523.             raise ValueError()
  524.         self.currentSize = start
  525.         self.totalSize = totalSize
  526.  
  527.     def onDownloadError(self, error):
  528.         if self.restartOnError:
  529.             self.restartOnError = False
  530.             self.startDownload()
  531.         else:
  532.             self.client = None
  533.             self.handleNetworkError(error)
  534.  
  535.     def onBodyData(self, data):
  536.         if self.state != 'downloading':
  537.             return
  538.         self.updateRateAndETA(len(data))
  539.         downloadUpdater.queueUpdate(self)
  540.         try:
  541.             self.filehandle.write(data)
  542.         except IOError, e:
  543.             self.handleWriteError(e)
  544.  
  545.     def onDownloadFinished(self, response):
  546.         self.client = None
  547.         try:
  548.             self.filehandle.close()
  549.         except Exception, e:
  550.             self.handleWriteError(e)
  551.             return
  552.         self.state = "finished"
  553.         if self.totalSize == -1:
  554.             self.totalSize = self.currentSize
  555.         self.endTime = clock()
  556.         try:
  557.             self.moveToMoviesDirectory()
  558.         except IOError, e:
  559.             self.handleWriteError(e)
  560.         self.resetBlockTimes()
  561.         self.updateClient()
  562.  
  563.     def getStatus(self):
  564.         data = BGDownloader.getStatus(self)
  565.         data['dlerType'] = 'HTTP'
  566.         return data
  567.  
  568.     ##
  569.     # Update the download rate and eta based on recieving length bytes
  570.     def updateRateAndETA(self, length):
  571.         now = clock()
  572.         self.currentSize = self.currentSize + length
  573.         self.blockTimes.append((now,  self.currentSize))
  574.         window_start = now - self.UPDATE_CLIENT_WINDOW
  575.         i = 0
  576.         for i in xrange (len(self.blockTimes)):
  577.             if self.blockTimes[0][0] >= window_start:
  578.                 break
  579.         self.blockTimes = self.blockTimes[i:]
  580.  
  581.     ##
  582.     # Checks the download file size to see if we can accept it based on the 
  583.     # user disk space preservation preference
  584.     def acceptDownloadSize(self, size):
  585.         accept = True
  586.         if config.get(prefs.PRESERVE_DISK_SPACE):
  587.             if size < 0:
  588.                 size = 0
  589.             preserved = config.get(prefs.PRESERVE_X_GB_FREE) * 1024 * 1024 * 1024
  590.             available = platformutils.getAvailableBytesForMovies() - preserved
  591.             accept = (size <= available)
  592.         return accept
  593.  
  594.     ##
  595.     # Pauses the download.
  596.     def pause(self):
  597.         if self.state != "stopped":
  598.             self.cancelRequest()
  599.             self.state = "paused"
  600.             self.updateClient()
  601.  
  602.     ##
  603.     # Stops the download and removes the partially downloaded
  604.     # file.
  605.     def stop(self, delete):
  606.         if self.state == "downloading":
  607.             if self.filehandle is not None:
  608.                 try:
  609.                     if not self.filehandle.closed:
  610.                         self.filehandle.close()
  611.                     remove(self.filename)
  612.                 except:
  613.                     pass
  614.         if delete:
  615.             try:
  616.                 if os.path.isdir(self.filename):
  617.                     shutil.rmtree(self.filename)
  618.                 else:
  619.                     remove(self.filename)
  620.             except:
  621.                 pass
  622.         self.currentSize = 0
  623.         self.cancelRequest()
  624.         self.state = "stopped"
  625.         self.updateClient()
  626.  
  627.     def stopUpload(self):
  628.         # HTTP downloads never upload.
  629.         pass
  630.  
  631.     ##
  632.     # Continues a paused or stopped download thread
  633.     def start(self):
  634.         if self.state in ('paused', 'stopped', 'offline'):
  635.             self.state = "downloading"
  636.             self.startDownload()
  637.  
  638.     def shutdown(self):
  639.         self.cancelRequest()
  640.         self.updateClient()
  641.  
  642. class BTDownloader(BGDownloader):
  643.     def __init__(self, url = None, item = None, restore = None):
  644.         self.metainfo = None
  645.         self.torrent = None
  646.         self.rate = self.eta = 0
  647.         self.upRate = self.uploaded = 0
  648.         self.activity = None
  649.         self.fastResumeData = None
  650.         self.retryDC = None
  651.         self.channelName = None
  652.         self.uploadedStart = 0
  653.         self.restarting = False
  654.         if restore is not None:
  655.             self.restoreState(restore)
  656.         else:
  657.             BGDownloader.__init__(self,url,item)
  658.             self.runDownloader()
  659.  
  660.     def _shutdownTorrent(self):
  661.         try:
  662.             if self.torrent is not None:
  663.                 self.fastResumeData = self.torrent.shutdown()
  664.         except:
  665.             logging.exception ("DTV: Warning: Error shutting down torrent")
  666.  
  667.     def _startTorrent(self):
  668.         try:
  669.             self.torrent = bittorrentdtv.TorrentDownload(self.metainfo,
  670.                     self.filename, self.fastResumeData)
  671.         except:
  672.             self.handleError(_('BitTorrent failure'), 
  673.                     _('BitTorrent failed to startup'))
  674.         else:
  675.             self.torrent.set_status_callback(self.updateStatus)
  676.             self.torrent.start()
  677.  
  678.     @eventloop.asIdle
  679.     def updateStatus(self, newStatus):
  680.         """
  681.         activity -- string specifying what's currently happening or None for
  682.                 normal operations.  
  683.         upRate -- upload rate in B/s
  684.         downRate -- download rate in B/s
  685.         upTotal -- total MB uploaded
  686.         downTotal -- total MB downloaded
  687.         fractionDone -- what portion of the download is completed.
  688.         timeEst -- estimated completion time, in seconds.
  689.         totalSize -- total size of the torrent in bytes
  690.         """
  691.  
  692.         self.totalSize = newStatus['totalSize']
  693.         self.rate = newStatus['downRate']
  694.         self.upRate = newStatus['upRate']
  695.         self.uploaded = newStatus['upTotal'] + self.uploadedStart
  696.         self.eta = newStatus['timeEst']
  697.         self.activity = newStatus['activity']
  698.         self.currentSize = int(self.totalSize * newStatus['fractionDone'])
  699.         if self.state == "downloading" and newStatus['fractionDone'] == 1.0:
  700.             self.moveToMoviesDirectory()
  701.             self.state = "uploading"
  702.             self.endTime = clock()
  703.             self.updateClient()
  704.         else:
  705.             downloadUpdater.queueUpdate(self)
  706.  
  707.     def handleError(self, shortReason, reason):
  708.         self._shutdownTorrent()
  709.         BGDownloader.handleError(self, shortReason, reason)
  710.  
  711.     def handleTemporaryError(self, shortReason, reason):
  712.         self._shutdownTorrent()
  713.         BGDownloader.handleTemporaryError(self, shortReason, reason)
  714.  
  715.     def moveToDirectory(self, directory):
  716.         if self.state in ('uploading', 'downloading'):
  717.             self._shutdownTorrent()
  718.             BGDownloader.moveToDirectory(self, directory)
  719.             self._startTorrent()
  720.         else:
  721.             BGDownloader.moveToDirectory(self, directory)
  722.  
  723.     def restoreState(self, data):
  724.         self.__dict__.update(data)
  725.         self.rate = self.eta = 0
  726.         self.upRate = 0
  727.         self.uploadedStart = self.uploaded
  728.         if self.state in ('downloading', 'uploading'):
  729.             self.runDownloader(done=True)
  730.         elif self.state == 'offline':
  731.             self.start()
  732.  
  733.     def getStatus(self):
  734.         data = BGDownloader.getStatus(self)
  735.         data['upRate'] = self.upRate
  736.         data['uploaded'] = self.uploaded
  737.         data['metainfo'] = self.metainfo
  738.         data['fastResumeData'] = self.fastResumeData
  739.         data['activity'] = self.activity
  740.         data['dlerType'] = 'BitTorrent'
  741.         return data
  742.  
  743.     def getRate(self):
  744.         return self.rate
  745.  
  746.     def getETA(self):
  747.         return self.eta
  748.         
  749.     def pause(self):
  750.         self.state = "paused"
  751.         self._shutdownTorrent()
  752.         self.updateClient()
  753.  
  754.     def stop(self, delete):
  755.         self.state = "stopped"
  756.         self._shutdownTorrent()
  757.         self.updateClient()
  758.         if delete:
  759.             try:
  760.                 if os.path.isdir(self.filename):
  761.                     shutil.rmtree(self.filename)
  762.                 else:
  763.                     remove(self.filename)
  764.             except:
  765.                 pass
  766.  
  767.     def stopUpload(self):
  768.         self.state = "finished"
  769.         self._shutdownTorrent()
  770.         self.updateClient()
  771.  
  772.     def start(self):
  773.         if self.state not in ('paused', 'stopped', 'offline'):
  774.             return
  775.  
  776.         self.state = "downloading"
  777.         if self.retryDC:
  778.             self.retryDC.cancel()
  779.             self.retryDC = None
  780.         self.updateClient()
  781.         self.getMetainfo()
  782.  
  783.     def shutdown(self):
  784.         self._shutdownTorrent()
  785.         self.updateClient()
  786.  
  787.     def gotMetainfo(self):
  788.         # FIXME: If the client is stopped before a BT download gets
  789.         #        its metadata, we never run this. It's not a huge deal
  790.         #        because it only affects the incomplete filename
  791.         if not self.restarting:
  792.             try:
  793.                 metainfo = bdecode(self.metainfo)
  794.                 name = metainfo['info']['name']
  795.             except (ValueError, KeyError):
  796.                 self.handleError(_("Corrupt Torrent"),
  797.                         _("The torrent file at %s was not valid") % stringify(self.url))
  798.                 return
  799.             name = name.decode('utf-8', 'replace')
  800.             self.shortFilename = cleanFilename(name)
  801.             self.pickInitialFilename()
  802.         self.updateClient()
  803.         self._startTorrent()
  804.  
  805.     def handleMetainfo(self, metainfo):
  806.         self.metainfo = metainfo
  807.         self.gotMetainfo()
  808.  
  809.     def onDescriptionDownload(self, info):
  810.         self.handleMetainfo (info['body'])
  811.  
  812.     def onDescriptionDownloadFailed(self, exception):
  813.         self.handleNetworkError(exception)
  814.  
  815.     def getMetainfo(self):
  816.         if self.metainfo is None:
  817.             if self.url.startswith('file://'):
  818.                 path = getFileURLPath(self.url)
  819.                 try:
  820.                     metainfoFile = open(path, 'rb')
  821.                 except IOError:
  822.                     self.handleError(_("Torrent file deleted"),
  823.                             _("The torrent file for this item was deleted "
  824.                                 "outside of Miro."))
  825.  
  826.                     return
  827.                 try:
  828.                     metainfo = metainfoFile.read()
  829.                 finally:
  830.                     metainfoFile.close()
  831.  
  832.                 self.handleMetainfo(metainfo)
  833.             else:
  834.                 httpclient.grabURL(self.getURL(), self.onDescriptionDownload,
  835.                         self.onDescriptionDownloadFailed)
  836.         else:
  837.             self.gotMetainfo()
  838.                 
  839.  
  840.     def runDownloader(self,done=False):
  841.         self.restarting = done
  842.         self.updateClient()
  843.         self.getMetainfo()
  844.